package gaml.java.serializer;

import gaml.error.GamlException;
import gaml.java.ClassSerializer;
import gaml.java.ClassSerializerStore;
import gaml.java.NodeObjectBridge;
import gaml.java.SuperFactory;
import gaml.yaml.MappingNode;
import gaml.yaml.ScalarNode;
import gaml.yaml.YamlNode;
import gaml.yaml.ahchor.YamlAnchor;
import gaml.yaml.ahchor.YamlAnchorCodeStore;
import gaml.yaml.ahchor.YamlAnchorObjectStore;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;


/**
 * serialization for complex object;
 * serialize it as a mapping for object field values
 * @author shnireck
 *
 * @param <T> object class type
 */
public class ComplexSerializer <T> implements ClassSerializer <T> {
	static final String baseAncestorKey = ".this";
	
	ClassSerializerStore store = null;
	Class <T> clazz = null;
	
	public ComplexSerializer (ClassSerializerStore store, Class <T> clazz) {
		this.store = store;
		this.clazz = clazz;
	}
	
	@Override
	public Class <T> getObjectClass () { return clazz;}
	
	@Override
	public boolean noAnchor () { return false; }
	
	@Override
	public YamlNode serialize (YamlNode node, T object, YamlAnchorObjectStore anchors) throws GamlException {
		NodeObjectBridge <?> ancestor = NodeObjectBridge.create (node, object, clazz.getSuperclass (), store); // serialize data fields from object's class ancestor
		defineAncestorNode (separateBaseAncestor (ancestor, node, anchors).parseObject (anchors), node);

		for (Field field: clazz.getDeclaredFields ()) // serialize data from object's class fields
			if ((! field.getName ().startsWith ("this$")) && (! (Modifier.isStatic (field.getModifiers ())))) {
				field.setAccessible (true);
				try { node.addChild (new ScalarNode (node).setValue (field.getName ()), NodeObjectBridge.create (field.get (object), node, (Class <Object>) cls (field, object), store).parseObject (anchors).getNode ().setTag (getTypeTag (field, object))); } 
				catch (IllegalArgumentException e) { throw new GamlException (e); }
				catch (IllegalAccessException e) { throw new GamlException (e); }
			}

		return node;
	}

	@Override
	public T restore (YamlNode node, T object, YamlAnchorCodeStore anchors) throws GamlException {
		NodeObjectBridge <?> ancestor = NodeObjectBridge.create (node, object, clazz.getSuperclass (), store);
		if (store.contains (clazz.getSuperclass ())) if (hasKey (node, baseAncestorKey)) ancestor.setNode (node.getChild (getKey (node, baseAncestorKey)));
		ancestor.parseNode (anchors); // get data for superclasses of serializing class from superclasses

		for (Field field: clazz.getDeclaredFields ()) // get data for every class'es field from node
			if ((! field.getName ().startsWith ("this$")) && (! (Modifier.isStatic (field.getModifiers ())))) {
				field.setAccessible (true);
				try { field.set (object, NodeObjectBridge.create (getChild (node, field.getName ()), (getChild (node, field.getName ()).getTag ().isEmpty () ? field.getType () : Class.forName (getChild (node, field.getName ()).getTag ())), store).parseNode (anchors).getObject ()); }
				catch (ClassNotFoundException e) { throw new GamlException (String.format ("%s, deserialize [%s] from class [%s]", e, field.getName (), clazz.getName ())); }
				catch (IllegalArgumentException e) { throw new GamlException (String.format ("%s, deserialize [%s] from class [%s]", e, field.getName (), clazz.getName ())); }
				catch (IllegalAccessException e) { throw new GamlException (String.format ("%s, deserialize [%s] from class [%s]", e, field.getName (), clazz.getName ())); }
			}
		return object;
	}

	@Override
	public YamlNode createNode (YamlNode parent) { return new MappingNode (parent); }

	@Override
	public T createObject () throws GamlException { return (clazz.isEnum () ? null : SuperFactory.getInstance (clazz)); }
	
	/**
	 * if we, moving throw ancestors, reached the last one, 
	 * make a separate node to save node's value
	 * @param ancestor bridge to ancestor values
	 * @param node base parsing node
	 * @return ancestor with new separate node, if no more retrospection, unchanged ancestor otherwise
	 * @throws GamlException object parsing error
	 */
	protected NodeObjectBridge <?> separateBaseAncestor (NodeObjectBridge <?> ancestor, YamlNode node, YamlAnchorObjectStore anchors) throws GamlException {
		if (store.contains (clazz.getSuperclass ())) {	
			YamlNode value = ancestor.getSerializer ().createNode (node);
			value.setAnchor (new YamlAnchor ("", value));
			ancestor.setNode (value);
		}
		
		return ancestor;
	}
	
	/**
	 * if we have a separate non-empty node in
	 * ancestor node with base ancestor data - add it as
	 * a sub-node for node
	 * @param <E> ancestor bridge type
	 * @param program bridge between yaml node and
	 * ancestor's class object data fields
	 * @param node node to use in bridge
	 * @return host parend bridge object
	 */
	protected <E> NodeObjectBridge <E> defineAncestorNode (NodeObjectBridge <E> ancestor, YamlNode node) {
		if (! ancestor.getNode ().isEmpty ())
			if (! node.equals (ancestor.getNode ()))
				node.addChild (new ScalarNode (node).setValue (baseAncestorKey), ancestor.getNode ().setAnchor (new YamlAnchor ("", ancestor.getNode ())));

		return ancestor;
	}
	
	/**
	 * check if mapping node has a key with specified string value
	 * @param node mapping node to check
	 * @param keyValue string value for key to search
	 * @return true if key with string value found, false if no such key
	 */
	protected boolean hasKey (YamlNode node, String keyValue) { for (YamlNode key: node.getMappingChildren ().keySet ()) if (keyValue.equals (key.getValue ())) return true; return false; }
	
	/**
	 * find a key yaml node in mapping node by it's string value
	 * @param node node to search in
	 * @param keyValue string value of key node
	 * @return key Yaml Node
	 * @throws GamlException node for value not found
	 */
	protected YamlNode getKey (YamlNode node, String keyValue) throws GamlException {
		StringBuilder keys = new StringBuilder ();
		for (YamlNode child: node.getMappingChildren ().keySet ()) { keys.append (child.getValue ()).append (";"); if (keyValue.equals (child.getValue ())) return child; }
		throw new GamlException (String.format ("Can't find key [%s] in node [%s] with keys [%s]", keyValue, node, keys.toString ()));
	}
	
	/**
	 * find a child value node in mapping node by it's key string value
	 * @param node mapping node to search in
	 * @param keyValue string value of key node
	 * @return value node, referenced by key node with string value
	 * @throws GamlException value not found
	 */
	protected YamlNode getChild (YamlNode node, String keyValue) throws GamlException {
		return node.getChild (getKey (node, keyValue));
	}
	
	protected String getTypeTag (Field field, Object object) throws GamlException {
		if  ((null == cls (field, object)) || (field.getType ().equals (cls (field, object)))) return "";
		if (store.contains (cls (field, object)))
			if (store.get (cls (field, object)).equals (store.get (field.getType ()))) return "";
		return cls (field, object).getName ();
	}
	
	/**
	 * retrieves class for object's data member
	 * @param <E> type of object
	 * @param field object's field to get it's content class
	 * @param object java object to retrieve it's member's class
	 * @return java class for object's member
	 * @throws GamlException 
	 */
	protected <E> Class <E> cls (Field field, E object) throws GamlException {
		try { return (Class <E>) (null == field.get (object) ? null : field.get (object).getClass ()); }
		catch (IllegalArgumentException e) { throw new GamlException (e); }
		catch (IllegalAccessException e) { throw new GamlException (e); }		
	}
}